函数是一个命名了的代码块,我们通过调用函数执行相应的代码。函数可以有0个或多个参数,而且(通常)会产生一个返回值。可以重载函数,也就是说一个名字可以对应几个不同的函数。

6.1 函数基础

典型的函数包含:返回类型、函数名字、由0个或多个形参组成的列表以及函数体。

通过调用运算符来执行函数,调用运算符是一对圆括号,作用于一个表达式,该表达式是函数或者指向函数的指针。

函数的调用完成两项工作:

  1. 用实参初始化函数的形参,执行函数的第一步是(隐式地)定义并初始化它的形参
  2. 将控制权转移给调用函数,此时,主调用函数的执行暂时中断,被调函数开始执行

当遇到一条return语句时函数结束执行过程,完成两项工作:

  1. 返回return语句中的值(如果有的话),函数的返回值用于初始化调用表达式的结果
  2. 将控制权从被调函数转移回主调函数

实参是形参的初始值,尽管实参和形参存在对应关系,但并没有规定实参的求值顺序,编译器能以任意可行的顺序对实参求值。实参类型必须与形参类型匹配(可以存在隐式转换),函数有几个形参就必须提供相同数量的实参,因为函数的调用规定实参数量应与形参数量一致,所以形参一定会被初始化。

函数的形参列表可以为空,但是不能省略:

void f1(){/*...*/}	//隐式地定义空形参列表
void f2(void){/*...*/}	//显式地定义空形参列表,为了与C语言兼容

形参列表中形参通过逗号隔开,每个形参都有类型声明符,即使两个形参一样,也不可省略。任意两个形参不能同名。

大多数类型都能用作函数的返回类型。一种特殊的返回类型是void,它表示函数不返回任何值,函数返回值不能是数组类型或函数类型,但是可以是指向数组或函数的指针。

6.1.1 局部对象

在C++语言中,名字有作用域,对象有生命周期

  • 名字的作用域是程序文本的一部分,名字在其中可见
  • 对象的生命周期是程序执行过程中该对象存在的一段时间

函数体是一个块,块构成一个新的作用域。形参和函数体内部定义的变量统称局部变量,仅在函数的作用域内可见,局部变量会隐藏在外层作用域中同名的其他所有声明中。

只存在于块执行期间的对象称为自动对象。形参是一种自动对象,传递给函数的实参初始化形参对应的自动对象,对于举报变量对应的自动对象来说,分两种情况:

  1. 变量定义本身含有初始值,则使用初始值进行初始化
  2. 变量定义本身不含初始值,执行默认初始化。意味着内置类型的未初始化局部变量将产生未定义的值

局部静态变量:在程序执行路径第一次经过对象定义语句时初始化,直到程序终止才被销毁,通过将变量定义成 static类型得到。如果局部静态变量没有显示的初始值,将执行默认初始化,内置类型的局部静态变量初始化为0。

6.1.2 函数声明

和其他名字一样,函数的名字必须在使用之前声明。函数只能定义一次,但是可以声明多次。如果一个函数永远不会被我们用到,那么它可以只有声明没有定义。

函数的声明和定义非常相似,唯一的区别是函数声明无须函数体,用一个分号代替即可。

因为函数的声明不包含函数体,所以也就无须形参名字。在函数声明中经常省略形参的名字,尽管如此,写上形参名字有助于理解函数功能。

函数的三要素(返回类型、函数名、形参类型)描述了函数的接口,说明了调用该函数所需的全部信息。函数声明也称作函数原型

建议变量、函数在头文件中声明,在源文件中定义。定义函数的源文件应该把含有函数声明的头文件包含进来,编译器负责验证函数的定义和声明是否匹配。

6.1.3 分离式编译

为了允许编写程序时按照逻辑关系将其划分开来,C++语言支持所谓的分离式编译。分离式编译允许我们把程序分割到几个文件中去,每个文件独立编译。

如果我们修改了其中一个源文件,那么只需要重新编译那个改动了的文件。大多数编译器提供了分离式编译每个文件的机制,这一过程通常会产生一个后缀名是.obj(WIndows)或.o(Unix)的文件,后缀名的含义是该文件包含对象代码。

编译器负责把对象文件链接形成可执行文件。

6.2 参数传递

每次调用函数时会重新创建它的形参,并用传入的实参进行初始化。

和其他变量一样,形参的类型决定了形参和实参交互的方式。如果形参是引用类型,它将绑定到对应的实参上;否则,将实参的值拷贝后赋给形参。

当形参是引用类型时,我们说它对应的实参被引用传递或者函数被传引用调用。和其他引用一样,引用形参也是它绑定的对象的别名;也就是说,引用形参是它对应的实参的别名。

当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象。我们说这样的实参被值传递或者函数被传值调用

6.2.1. 传值参数

函数对形参所做的所有操作都不会影响实参。

指针的行为和其他非引用类型一样,当执行指针拷贝操作时,拷贝的是指针的值。拷贝之后,两个指针是不同的指针,因为指针使我们可以间接地访问它所指的对象,所以通过指针可以修改它所指对象的值。

熟悉C的程序员常常使用指针类型的形参访问函数外部的对象。在C++语言中,建议使用引用类型的形参代替指针。

6.2.2 传引用参数

通过使用引用形参,允许函数改变一个或多个实参的值。

使用引用可以避免拷贝:对有些大的类类型对象或者容器对象进行拷贝是比较低效的,甚至有的类型(包括IO类型在内)根本不支持拷贝操作。当某种类型不支持拷贝操作时,函数只能通过引用形参访问改类型的对象。

如果函数无须改变引用形参的值,最好将其声明为常量引用。

一个函数每次只能返回一个值,然而有时候函数同时返回多个值,引用形参为我们一次返回多个结果提供了有效途径。

6.2.3 const形参和实参

顶层const作用于对象本身,和其他初始化过程一样,当用实参初始化形参时会忽略掉顶层const。即当形参有顶层const时,传给它常量对象或者非常量对象都是可以的。

在C++语言中,允许我们定义若干具有相同名字的函数,但是不同函数的形参列表应该有明显的区别。

void fcn(const int i){}
void fcn(int i){}	//错误:重复定义了fcn(int)

我们可以使用非常量初始化一个底层const对象,但是反过来不行;同时一个普通的引用必须用同类型的对象初始化。C++允许我们用字面值初始化常量引用。尽量使用常量引用,可以避免一些不经意的修改。

6.2.4 数组形参

数组的两个特殊性质对我们定义和使用作用在数组上的函数有影响,这两个性质分别是:

  1. 不允许拷贝数组
  2. 使用数组时(通常)会将其转换成指针

因为不能拷贝数组,所以我们无法以值传递的方式使用数组参数,因为数组会转换成指针,所以我们为函数传递一个数组时,实际上传递的是执行数组首元素的指针。

尽管不能以值传递的方式传递数组,但是我们可以把形参写出类似数组的形式:

//尽管形式不同,但这三个printf函数是等价的
//每个函数都有一个const int* 类型的形参
void printf(const int*);
void printf(const int[]);		//可以看出函数的意图是作用于一个数组
void printf(const int[10]);		//这里的维度表示我们期望数组含有多少元素,实际不一定

当我们传给printf函数一个数组,则实参自动地转换成指向数组首元素的指针,数组的大小对函数的调用没有影响。和其他使用数组的代码一样,以数组作为形参的函数也必须确保使用数组时不会越界。

因为数组是以指针的形式传递给函数的,所以一开始函数并不知道数组的确切尺寸,调用者应该为此提供一些额外的信息。管理指针形参有三种常用的技术:

  1. 使用标记指定数组长度:要求数组本身包含一个结束标记,使用这种方法的典型示例是C风格字符串
  2. 使用标准库规范:传递指向数组首元素和尾后元素的指针,标准库begin和end函数提供所需指针
  3. 显示传递一个表示数组大小的形参

C++语言允许将变量定义成数组的引用,形参也可以是数组的引用,此时,引用形参绑定到对应的实参上,也就是绑定到数组上。因为数组大小是构成数组类型的一部分,所以形参声明的数组维度必须与实参的数组维度一致。

C++实际上没有真正的多维数组,和所有数组一样,当多维数组传递给函数时,真正传递的是指向数组首元素的指针。

void print(int (*martix)[10], int rowSize){}	//指向含有10个整数的数组的指针
//等价于
void print(int matrix[][10], int rowSize){}
//第一个维度会被编译器忽略,所以最好不要把它包括在形参列表内

int *matrix[10];	//10个指针构成的数组
int (*matrix)[10];	//指向含有10个整数的数组的指针

6.2.5 main:处理命令行选项

int main(int argc, char *argv[]){}

第二个形参argv是一个数组,它的元素是指向C风格字符串的指针;第一个形参argc表示数组中字符串的数量。

当实参传给main函数后,argv第一个元素指向程序的名字或者一个空字符串,接下来的元素依次传递命令行提供的实参。最后一个指针之后的元素值保证为0。

当使用argv中的实参时,一定要记得可选的实参从argv[1]开始;argv[0]保存程序的名字,而非用户输入。

6.2.6 含有可变形参的函数

有时候我们无法提前预知应该向函数传递几个实参,为了编写能处理不同数量实参的函数,C++11新标准提供了两种主要的方法:

  1. 如果所有的实参类型相同,可以传递一个名为initializer_list的标准库类型
  2. 如果实参的类型不同,我们可以编写一种特殊的函数,也就是所谓的可变参数模板

C++还有一种特殊的形参类型(即省略符),可以用它传递可变数量的实参,一般只用于与C函数交互的接口程序。

如果函数的实参数量未知但是全部实参的类型都相同,我们可以使用initializer_list类型的形参,initializer_list是一种标准库类型,用于表示某种特定类型的值的数组,定义在同名头文件中。

image-20200617175355468

和vector一样是模板类型,但是initializer_list对象中的元素永远是常量值,无法改变。如果想向initial

6.3 返回类型和return语句

return语句终止当前正在执行的函数并将控制权返回到调用该函数的地方。

6.3.1 无返回值函数

没有返回值的return语句只能用在返回类型是void的函数中。返回void的函数不要求一定要有return语句,因为在这类函数的最后会隐式地执行return。

通常情况,void函数如果想要在函数中间提前退出,可以使用return语句。

一个返回类型是void的函数可以返回一个返回void的函数。强行令void函数返回其它类型的表达式将产生编译错误。

6.3.2 有返回值函数

只要函数的返回类型不是void,则函数内每条return语句必须返回一个值,值的类型必须和返回类型一致,或能隐式地转换。

C++无法确保结果的正确性,但保证每个return语句的结果类型正确。编译器尽量确保具有返回值的函数只能通过一条有效的return语句退出。

在含有return语句的循环后面应该也有一条return语句,如果没有的话该程序就是错误的。

返回一个值的方式和初始化一个变量或形参的方式完全一样:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。

//如果ctr的值大于1,返回word的复数形式
string make_plural(size_t ctr, const string &word, const string &ending){
    return (ctr > 1) ? word + ending : word;
}

函数返回类型是string,返回值将被拷贝到调用点,该函数返回word的副本或一个未命名的临时string对象。

同其它引用类型一样,如果函数返回引用,则该引用仅是它所引对象的一个别名:

//挑出两个string对象中较短的那个,返回其引用
const string &shortString(const string &s1, const string &s2){
    return s1.size() <= s2.size() ? s1 : s2;
}

形参和返回类型都是const string的引用,不管是调用函数还是返回结果都不会真正拷贝string对象。

不要返回局部对象的引用或指针,函数完成后,它所占用的存储空间将被释放,函数终止意味着局部变量的引用或指针将指向无效的内存区域。

调用运算符的优先级与点运算符和箭头运算符相同,并且符合左结合律:

auto sz = shortString(s1, s2).size();

**函数返回类型决定函数是否是左值:**调用一个返回引用的函数得到左值,其它返回类型得到右值。可以像使用其它左值那样来使用返回引用的函数调用:

char &get_val(string &str, string::size_type ix){
	return str[ix];
}
int main(){
    string s("a value");
    cout << s << end;
    get_val(s, 0) = 'A';
    cout << s << endl;
    return 0;
}

如果返回的是常量引用则不能给调用的结果赋值。

C++11新标准规定,函数可以返回花括号包围的值的列表,类似其它返回结果,此列表也用来对表示函数返回的临时量进行初始化,如果列表为空,临时量执行值初始化,否则,返回的值由函数的返回类型决定。

如果函数返回的是内置类型,则花括号包围的列表最多包含一个值,而且该值所占空间不应该大于目标类型的空间。如果函数返回的是类类型,由类本身定义初始值如何使用。

如果main函数的返回类型不是void,一般函数必须返回一个值,但是允许main函数没有返回语句直接结束:如果控制到达了main函数的结尾处而且没有return语句,编译器将隐式地插入一条返回0的return语句。

main函数的返回值可以看作是状态指示器:返回0表示执行成功,返回其它值表示执行失败,非0值的具体含义依机器而定。为了使返回值与机器无关,cstdlib头文件定义了两个预处理变量,分别表示成功与失败:

int main(){
	if(some_failure)
        return EXIT_FAILURE;
    else
        return EXIT_SUCCESS;
}

因为它们是预处理变量,不能在前面加上std::,也不能在using声明中出现。

当一个函数调用了它自身,不管调用是直接的还是间接的,都称该函数为递归函数。在递归函数中,一定有某条路径是不包含递归调用的,否则函数将不断调用它自身直到程序栈空间耗尽为止。有时候会称这种函数含有递归循环。

6.3.3 返回数组指针

因为数组不能被拷贝,所有函数不能返回数组,但可以返回数组的指针或引用。

使用类型别名简化声明:

typedef int arrT[10];//arrT是一个类型别名,它表示的类型是含有10个整数的数组
using arrT = int[10];//等价
arrT* func(int i);	//func返回一个指向含有10个整数的数组的指针
int arr[10];	//含有10个整数的数组
int *p1[10];	//含有10个整型指针的数组
int (*p2)[10] = &arr;	//p2是一个指针,指向含有10个整数的数组

因此,声明一个返回数组指针的函数的格式如:

Type (*function(paramcter_list))[dimension]

Type:表示元素的类型

dimension:表示数组的大小

例子:int (*func(int i))[10];

  • func(int i)表示调用func函数时需要一个int类型的实参
  • (*func(int i))意味着我们可以对函数调用的结果执行解引用操作
  • (*func(int i))[10]表示解引用func的调用将得到一个大小是10的数组
  • int (*func(int i))[10]表示数组中的元素是int类型

C++11新标准可以简化上述func声明的方法,就是使用尾置返回类型。任何函数的定义都能使用尾置返回,对于返回类型复杂的函数最有效。尾置返回类型跟在形参列表后面以一个->符号开头:

auto func(int i) -> int (*)[10]

如果我们知道函数返回的指针指向哪个数组,就可以使用decltype关键字声明返回类型:

int odd[] = {1,3,5,7,9};
int even[] = {0,2,4,6,8};
//返回一个指针,该指针指向含有5个整数的数组
decltype(odd) *arrPtr(int i){
    return (i % 2) ? &odd : &even;	//返回一个指向数组的指针
}

6.4 函数重载

如果一个作用域内的几个函数名字相同但形参列表不同,称之为重载函数。重载函数的形参类型不一样,但是执行的操作非常类似。

函数的名字仅仅是让编译器知道它是哪个函数,而函数重载可以在一定程度上减轻程序员起名字、记名字的负担。

main函数不能重载。重载函数应该在形参数量或形参类型上有所不同。不允许两个函数除了返回类型外其它所有的要素都相同。两个函数,形参列表一样但是返回类型不同,那么第二个函数的声明是错误的。

有时候两个形参列表看起来不一样,但实际上是相同的:

//每对声明的是同一函数
Record lookup(const Account &acct);
Record lookup(const Account &);//形参名字仅仅起到帮助记忆的作用,不影响形参列表的内容

typedef Phone Telno;//类型别名,实质相同
Record lookup(const Phone &);
Record lookup(const Telno &);

顶层const不影响传入函数的对象,一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来:

Record lookup(Phone);
Record lookup(const Phone);	//重复声明了Record lookup(Phone)

Record lookup(Phone*);
Record lookup(Phone* const);	//重复声明了Record lookup(Phone*)

如果形参是某种类型的指针或引用,则通过区分其执行的是常量对象还是非常量对象可以实现函数重载,即底层const可区分:

//对于接受引用或指针的函数来说,对象是常量还是非常量对应的形参不同
//定义了4个独立的重载函数
Record lookup(Account &);
Record lookup(const Account &);

Record lookup(Account *);
Record lookup(const Account *);

上面四个函数中,编译器可以通过实参是否是常量来推断应该调用哪个函数。因为const不能转换成其它类型,所以const对象只能传递给const形参;而非常量可以转换成const,但是编译器会优先选择非常量版本的函数。

函数重载最好是重载那些非常相似的操作,可以减轻命名负担。

const_cast在重载函数的情况下最有用:

const string &shorterString(const string &s1, const string &s2){
    return s1.size() <= s2.size() ? s1 : s2;
}

string &shorterString(string &s1, string &s2){
    auto &r = shorterString(const_cast<const string&>(s1), 
                            const_cast<cosnt string&>(s2));
    return const_cast<string&>(r);
}
//这样使用是安全的

定义了一组重载函数后,我们需要以合理的实参调用它们。函数匹配是指一个过程,在这个过程中我们把函数调用与一组重载函数中的某一个关联起来,函数匹配也叫做重载确定

调用重载函数的三种可能结果:

  • 编译器找到一个与实参最佳匹配的函数,并生成调用该函数的代码
  • 找不到任何一个函数与调用的实参匹配,此时编译器发出无匹配的错误信息
  • 有多于一个函数可以匹配,但是每个都不是明显的最佳选择,此时也将发生错误,称为二义性调用

6.4.1 重载与作用域

重载对作用域的一些性质并没有发生什么改变:如果我们在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体。在不同作用域中无法重载函数名。

string read();
void print(const string &);
void print(double);	//重载print函数
void fooBar(int ival);
{
    bool read = false;	//新作用域:隐藏外层read
    string s = read();	//错误:read是一个布尔值
    //不好的习惯:通常来说,局部作用域声明函数不是一个好的选择
    void print(int);	//新作用域:隐藏了之前的print
    print("Value: ");	//错误:void print(const string &)被隐藏了
    print(ival);	//正确:当前print(int)可见
    print(3.14);	//正确:调用局部print(int),隐藏了外层print(double)
}

当我们调用print函数时,编译器先寻找对该函数名的声明,一旦在当前作用域找到了所需的名字,编译器就会忽略外层作用域的同名实体,剩下的工作就是检查函数调用是否有效。

在C++语言中,名字查找发生在类型检查之前。

6.5 特殊用途语言特性

6.5.1 默认实参

某些函数有这样一种形参,在函数的很多次调用它们都被赋予一个相同的值,我们把这个反复出现的值称为函数的默认实参。调用含有默认实参的函数时,可以包含该实参,也可以省略该实参。

typedef string::size_type sz;
string screen(sz ht = 24, sz wid = 40, char backgrnd = ' ');

一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。

如果我们使用默认实参,只要在调用函数的时候省略该实参就可以了:

string window;
window = screen();				//等价window = screen(24, 80, ' ');
window = screen(66);			//等价window = screen(66, 80, ' ');
window = screen(66,256);		//等价window = screen(66, 256, ' ');
window = screen(66,256,'#');	//等价window = screen(66, 256, '#');

函数调用时实参按其位置解析,默认参数负责填补函数调用缺少的尾部实参(靠右侧位置)。设计含有默认实参的函数时,一项任务是合理设置形参的顺序,让那些经常使用默认值的形参出现在后面。

函数声明多次是合法的,因此,在给定的作用域中一个形参只能被赋予一次默认实参,函数的后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧的所有形参必须都已经有默认值。即默认实参可以分多次函数声明分别赋值,而且必须从右侧开始。一般一个函数只声明一次,而且声明放在头文件中。

局部变量不能作为默认实参,其它只要表达式的类型能转换成形参所需的类型,该表达式就能作为默认实参。

//wd、def和ht的声明必须出现在函数之外
sz wd = 80;
char def = ' ';
sz ht();
string screen(sz = ht(), sz = wd, char = def);
string window  = screen();	//调用screen(ht(), 80, ' ');

void f2(){
	def = '*';			//改变默认实参的值
    sz wd = 100;		//隐藏了外层定义的wd,但是没有改变默认值
    window = screen();	//调用screen(ht(), 80, '*')
}

6.5.2 内联函数和constexpr函数

在大多数机器上,一次函数调用包含一系列工作:调用前要先保存寄存器,并在返回时恢复;可能需要拷贝实参;程序转向一个新的位置继续执行。

内联函数可以避免函数调用的开销,将函数指定为内联函数,通常就是将它在每个调用点上“内联地展开”,消除函数运行时的开销。在函数定义的返回类型之前加上关键字inline即可将它声明为内联函数,但是,内联说明只是向编译器发出的一个请求,编译器可以选择忽略这个请求。

一般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数。

constexpr函数是指能用于常量表达式的函数。定义constexpr函数要遵循几项约定:函数的返回类型以及所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条return语句。

constexpr int new_sz(){return 42;}
constexpr int foo = new_sz();

执行初始化任务时,编译器把对constexpr函数的调用替换成其结果值。为了能在编译过程中随时展开,constexpr函数被隐式地定义为内联函数。

constexpr函数体可以包含其它语句,只要这些语句在运行时不执行任何操作就行,如空语句、类型别名、using声明。

允许constexpr函数的返回值并非一个常量:constexpr函数不一定返回常量表达式

constexpr size_t scale(size_t cnt){return new_sz()*cnt;}
//如果arg是常量表达式,则scale(arg)也是常量表达式
//当scale的实参是常量表达式时,它的返回值也是常量表达式,反之则不然
int arr[scale(2)];	//正确:scale(2)是常量表达式
int i = 2;			//i不是常量表达式
int a2[scale(i)];	//错误:scale(i)不是常量表达式

和其它函数不一样,内联函数和constexpr函数可以在程序中多次定义。不过,对于某个给定的内联函数或者constexpr函数来说,它的多个定义必须一致。基于这个原因,内联函数和constexpr函数通常定义在头文件中。

6.5.3 调试帮助

C++程序员有时会用到一种类似于头文件保护的技术,以便有选择地执行调试代码。基本思想是:程序包含一些用于调试的代码,但是这些代码只在开发程序时使用。当应用程序编写完成准备发布时,要先屏蔽掉调试代码。

这种方法用到两项预处理功能:assert和NDEBUG

assert是一种预处理宏。预处理宏是一个预处理变量,它的行为类似于内联函数。assert宏使用一个表达式作为它的条件:assert(expr)

对expr求值,如果表达式为假(即0),assert输出信息并终止程序执行;如果为真(即非0),assert什么也不做。

assert宏定义在cassert头文件中。预处理名字由处理器而非编译器管理,使用预处理名字无须提供using声明。和预处理变量一样,宏名字在程序内必须唯一,含有cassert头文件的程序不能再定义名为assert的变量、函数或其它实体。许多头文件都包含了cassert头文件,即使没有直接包含,也可能通过其它途径包含。

assert宏常用于检查“不能发生”的条件。

**assert的行为依赖于一个名为NDEBUG的预处理变量的状态。**如果定义了NDEBUG,则assert什么也不做。默认状态下没有定义NDEBUG,此时assert将执行运行时检查。

可以使用一个#define语句定义NDEBUG,从而关闭调试状态,也可通过编译器提供的选项定义预处理变量。

assert可以当成调试程序的一种手段,但是不能代替真正运行时的逻辑检查和程序应该包含的错误检查。

NDEBUG除了用于assert外,还可以用于编写条件调试代码:

void print(const int ia[], size_t size){
#ifndef NDEBUG
    //__func__是编译器定义的一个局部静态变量,用于存放函数名字
    cerr << __func__ << ": array size is " << size << endl;
#endif
//...
}

编译器为每个函数都定义了__func__,它是const char的一个静态数组,用于存放函数的名字。

C++编译器定义的其它四个对于程序调试很有用的名字:

  • __FILE__:存放文件名的字符串字面值
  • __LINE__:存放当前行号的整型字面值
  • __TIME__:存放文件编译时间的字符串字面值
  • __DATE__:存放文件编译日期的字符串字面值
  • __func__:存放函数的名字

6.6 函数匹配

void f();
void f(int);
void f(int, int);
void f(double, double = 3.14);
f(5.6);	//调用void f(double, double = 3.14)

函数匹配步骤:

  1. 选定本次调用对应的重载函数集,集合中的函数称为候选函数

    • 与被调用的函数同名
    • 其声明在调用点可见
  2. 考察本次调用提供的实参,从候选函数选出可行函数

    • 形参数量与调用提供的实参数量匹配
    • 每个实参的类型与对应的形参类型相同,或者能转换成形参类型
  3. 寻找最佳匹配(如果有的话):实参类型与形参类型越接近,匹配越好

    含有多个形参的函数匹配,编译器依次检查每个实参,如果有且只有一个函数满足条件则匹配成功:

    • 该函数每个实参的匹配都不劣于其它可行函数需要的匹配
    • 至少有一个实参的匹配优于其它可行函数提供的匹配

    即编译器检查的每个实参最匹配的函数是同一个才能匹配。如果不能,编译器最终因为调用具有二义性而聚集请求。看起来可以用强制类型转换来实现函数匹配,但是设计良好的系统中,不应该对实参进行强制类型转换。

6.6.1 实参类型转换

为了确定最佳匹配,编译器将实参类型到形参类型的转换划分成几个等级,具体排序为:

  1. 精确匹配
    1. 实参类型和形参类型相同
    2. 实参从数组类型或函数类型转换成对应的指针类型
    3. 向实参添加顶层const或者从实参中删除顶层const
  2. 通过const转换实现的匹配
  3. 通过类型提升实现的匹配
  4. 通过算术类型转换或指针转换实现的匹配
  5. 通过类类型转换实现的匹配

分析函数调用前,必须知道小整型一般都会提升到int类型或更大的整数类型。假设有两个函数,一个接受int,一个接受short,则只有当调用提供的是short类型的值时才会选择short版本的函数。然而有时候即使实参是一个很小的整数,也会直接提升为int整型,此时用short版本反而会导致类型转换:

void ff(int);
void ff(short);
ff('a');	//char提升为int,调用ff(int)

所有算术类型转换的级别都一样,从int向unsigned int转换并不比从int向double的转换高级:

void manip(long);
void manip(float);
manip(3.14);	//错误:二义性调用

如果重载函数的区别在于它们的引用类型的形参是否引用了const,或者指针类型的形参是否指向const,则当调用发生时编译器通过实参是否常量来决定选择哪个函数:

Record lookup(Account&);
Record lookup(const Account&);
const Account a;
Account b;

lookup(a);	//调用Record lookup(const Account&);
lookup(b);	//调用Record lookup(Account&);

第一个调用中,传入const对象,因为不能把普通引用绑定到const对象上,所以只有一个函数匹配。

第二个调用中,传入非常量对象b,两个函数都是可行函数,然而用非常量对象初始化常量引用需要类型转换,所以接受非常量引用的函数与b精确匹配。

指针类型的形参也类似:如果实参是指向常量的指针,调用形参是const*的函数;如果实参是指向非常量的指针,调用形参是普通指针的函数。

6.7 函数指针

函数指针指向的是函数而非对象,和其它指针一样,函数指针指向某种特定的类型,函数的类型由返回类型和形参类型共同决定,与函数名无关:

bool lengthCompare(const string &, const string &);

该函数的类型是bool(const string &, const string &)。要声明一个可以指向该函数的指针,只需要用指针替换函数名即可:

bool (*pf)(const string &, const string &);//返回类型是bool
bool *pf(const string &, const string &);//返回类型是bool*

//当我们把函数名作为一个值使用时,该函数自动地转换成指针
pf = lengthCompare;	//pf指向名为lengthCompare的函数
pf = lengthCompare;	//等价的赋值语句:取地址符是可选的

//通过函数指针调用该函数,无须提前解引用指针
bool b1 = pf("hello", "goodbye");	//调用lengthCompare函数
bool b2 = (*pf)pf("hello", "goodbye");//等价的调用:解引用符是可选的
bool b3 = lengthCompare("hello","goodbye");//另一个等价的调用

//可以为函数指针赋值nullptr或者值为0的常量表达式
//表示该指针没有指向任何一个函数
pf = 0;
pf = nullptr;

string::size_type sumLength(const string&, const string&);
bool castringCompare(const char*, const char*);
pf = sumLength;			//错误:返回类型不匹配
pf = cstringCompare;	//错误:形参类型不匹配
pf = lengthCompare;		//正确:函数和指针类型精确匹配

使用重载函数时,上下文应该清晰地界定到底应该使用哪个函数,如果定义了函数指针,编译器通过指针类型决定选用哪个函数,指针类型必须与重载函数在中的某一个精确匹配:

void ff(int*);
void ff(unsigned int);

void (*pf1)(unsigned int) = ff;	//pf1指向ff(unsigned)
void (*pf2)(int) = ff;			//错误:没有任何一个ff与该形参列表匹配
double (*pf2)(int*) = ff;		//错误:ff和pf3的返回类型不匹配

和数组类似,虽然不能定义函数类型的形参,但是形参可以是指向函数的指针,此时,形参看起来是函数类型,实际上却是当成指针使用:

//第3个参数是函数类型,它会自动转换成指向函数的指针
void useBigger(const string &s1, const string &s2, bool pf(const string &, const string &));
//等价的声明:显示地将形参定义成指向函数的指针
void useBigger(const string &s1, const string &s2, bool (*pf)(const string &, const string &));

//可以直接把函数作为实参使用,它会自动转换成指针
useBigger(s1, s2, lengthCompare);//自动将函数lengthCompare转换成指向函数的指针

直接使用函数指针类型显得冗长而烦琐,类型别名和decltype能简化使用函数指针:

//Func和Func2是函数类型
typedef bool Func(const string &, const string &);
typedef decltype(lengthCompare) Func2;	//等价的类型

//FuncP和FuncP2是指向函数的指针
typedef bool (*FuncP)(const string &, const string &);
typedef decltype(lengthCompare) *Func2;	//等价的类型

decltype返回函数类型,不会将函数类型自动转换成指针类型,只有在结果前面加上*才能得到指针。

可以使用如下的形式重新声明useBigger:

//useBigger的等价声明,使用了类型别名
void useBigger(const string &, const string &, Func);
void useBigger(const string &, const string &, FuncP2);

和数组类似,虽然不能返回一个函数,但是能返回一个指向函数类型的指针。然而,我们必须把返回类型写出指针形式,编译器不会自动地将函数返回类型当成对应的指针类型处理

要使用一个返回函数指针的函数,最简单的方法是使用类型别名:

using F = int(int*, int);		//F是函数类型,不是指针
using FF = int(*)(int*, int);	//FF是指针类型

FF f1(int);			//正确:FF是指向函数的指针,发f1返回指向函数的指针
F f1(int);			//错误:F是函数类型,f1不能返回一个函数
F *f1(int);			//正确:显示地指定返回类型是指向指向函数的指针

int (*f1(int))(int *, int);	//最原始的声明方法,由内而外阅读

由内而外的顺序阅读这条声明语句:f1有形参列表,所以f1是一个函数;f1前面有*,所以f1返回一个指针;指针的类型也包含形参列表,因此指针指向函数,该函数的返回类型是int。

除此外,还可使用尾置返回类型的方式声明一个返回函数指针的函数:

auto f1(int) -> int (*)(int*, int);

如果我们明确知道返回的函数是哪一个,就能使用decltype简化书写函数指针返回类型的过程:

string::size_type sumLength(const string &, const string &);
string::size_type largerLength(const string &, const string &);
decltype(sumLength) *getFcn(cosnt string &);